package cn.sciento.core.ldap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.util.*;

public class LdapUtil {
    private static final String INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";

    private static final String SECURITY_AUTHENTICATION = "simple";

    private static final Logger LOGGER = LoggerFactory.getLogger(LdapUtil.class);

    public static LdapContext authenticate(Ldap ldap) {
        String username = ldap.getAccount();
        String password = ldap.getPassword();
        String dirType = ldap.getDirectoryType();
        LdapContext ldapContext = ldapConnect(ldap);
        if (ldapContext == null)
            return null;
        if (dirType.equals(DirectoryType.OPEN_LDAP.value())) {
            String userDn = getUserDn(ldapContext, ldap, username);
            if (userDn.length() != 0 && ldapAuthenticate(ldapContext, userDn, password))
                return ldapContext;
        } else if (dirType.equals(DirectoryType.MICROSOFT_ACTIVE_DIRECTORY.value())) {
            try {
                ldapContext.addToEnvironment("java.naming.security.principal", username);
                ldapContext.addToEnvironment("java.naming.security.credentials", password);
                ldapContext.reconnect((Control[]) null);
                return ldapContext;
            } catch (NamingException e) {
                LOGGER.error("Microsoft Active Directory authenticate failed, exception : {}", e);
            }
        } else {
            LOGGER.error("not support directory type : {}", dirType);
        }
        return null;
    }

    public static LdapContext ldapConnect(Ldap ldap) {
        String url = ldap.getServerAddress();
        String baseDn = ldap.getBaseDn();
        String port = ldap.getPort();
        HashMap<String, String> ldapEnv = new HashMap<>(10);
        ldapEnv.put("java.naming.factory.initial", "com.sun.jndi.ldap.LdapCtxFactory");
        ldapEnv.put("java.naming.provider.url", url + ":" + port + "/" + baseDn);
        ldapEnv.put("java.naming.security.authentication", "simple");
        if (ldap.getUseSSL().booleanValue())
            ldapEnv.put("java.naming.security.protocol", "ssl");
        try {
            return new InitialLdapContext(new Hashtable<>(ldapEnv), null);
        } catch (NamingException e) {
            LOGGER.info("ldap connect fail: {}", e);
            return null;
        }
    }

    public static String getUserDn(LdapContext ldapContext, Ldap ldap, String username) {
        Set<String> attributeSet = initAttributeSet(ldap);
        if (attributeSet.contains(""))
            attributeSet.remove("");
        if (attributeSet.contains(null))
            attributeSet.remove(null);
        NamingEnumeration namingEnumeration = getNamingEnumeration(ldapContext, username, attributeSet);
        StringBuilder userDn = new StringBuilder();
        while (namingEnumeration != null && namingEnumeration.hasMoreElements()) {
            Object obj = namingEnumeration.nextElement();
            if (obj instanceof SearchResult) {
                SearchResult searchResult = (SearchResult) obj;
                userDn.append(searchResult.getName()).append(",").append(ldap.getBaseDn());
            }
        }
        return userDn.toString();
    }

    private static Set<String> initAttributeSet(Ldap ldap) {
        Set<String> attributeSet = new HashSet<>(Arrays.asList(new String[]{"employeeNumber", "mail", "mobile"}));
        if (ldap.getLoginNameField() != null)
            attributeSet.add(ldap.getLoginNameField());
        if (ldap.getRealNameField() != null)
            attributeSet.add(ldap.getRealNameField());
        if (ldap.getEmailField() != null)
            attributeSet.add(ldap.getEmailField());
        if (ldap.getPhoneField() != null)
            attributeSet.add(ldap.getPhoneField());
        return attributeSet;
    }

    public static NamingEnumeration getNamingEnumeration(LdapContext ldapContext, String username, Set<String> attributeSet) {
        SearchControls constraints = new SearchControls();
        constraints.setSearchScope(2);
        NamingEnumeration<SearchResult> namingEnumeration = null;
        for (String str : attributeSet) {
            try {
                namingEnumeration = ldapContext.search("", str + "=" + username, constraints);
                if (namingEnumeration.hasMoreElements())
                    break;
            } catch (NamingException e) {
                LOGGER.error("ldap search fail: {}", e);
            }
        }
        return namingEnumeration;
    }

    public static boolean ldapAuthenticate(LdapContext ldapContext, String userDn, String password) {
        try {
            ldapContext.addToEnvironment("java.naming.security.principal", userDn);
            ldapContext.addToEnvironment("java.naming.security.credentials", password);
            ldapContext.reconnect((Control[]) null);
        } catch (NamingException e) {
            LOGGER.info("ldap authenticate fail: {}", e);
            return false;
        }
        return true;
    }

    public static Attributes anonymousUserGetByObjectClass(Ldap ldap, LdapContext ldapContext) {
        SearchControls constraints = new SearchControls();
        constraints.setSearchScope(2);
        NamingEnumeration<SearchResult> namingEnumeration = null;
        try {
            namingEnumeration = ldapContext.search("", "objectClass=*", constraints);
            while (namingEnumeration != null && namingEnumeration.hasMoreElements()) {
                SearchResult searchResult = namingEnumeration.nextElement();
                Attributes attributes = searchResult.getAttributes();
                if (attributes.get("objectClass") != null && attributes
                        .get("objectClass").contains(ldap.getObjectClass()))
                    return attributes;
            }
        } catch (NamingException e) {
            LOGGER.info("ldap search fail: {}", e);
        }
        return null;
    }
}
